/**
 Created by:
 muhtearjan mehmood مۇختەرجان مەخمۇت (Email: yeganaaa@163.com)
 At 9/7/20 8:35 PM
*/

import { BridgeHostEvents, Completer, Fragment, IRpcBridgeHost, IRpcServerHost, ServerHostEvents } from "dream_rpc"
import { v4 as uuid } from "uuid"

enum MessageKind {
    CALL = 0x01,
    READY_REQUEST = 0x03,
    READY_RESPONSE = 0x04,
    CLOSED = 0x05,
}
abstract class Message {
    constructor(public clientId: string, public kind: MessageKind, public serverHost: string) {
    }
}
class ReadyRequestMessage extends Message {
    public kind: MessageKind = MessageKind.READY_REQUEST
    constructor(public clientId: string, serverHost: string) {
        super(clientId, MessageKind.READY_REQUEST, serverHost)
    }
}
class ReadyResponseMessage extends Message {
    constructor(public clientId: string, serverHost: string) {
        super(clientId, MessageKind.READY_RESPONSE, serverHost);
    }
}
class ClosedMessage extends Message {
    constructor(clientId: string, serverHost: string) {
        super(clientId, MessageKind.CLOSED, serverHost);
    }
}
class CallRequestMessage extends Message {
    constructor(clientId: string, public data: Uint8Array, serverHost: string) {
        super(clientId, MessageKind.CALL, serverHost);
    }
}
/**
 * Window bridge host that implemented {@link IRpcBridgeHost} interface through window.postMessage api.
 */
export class WindowBridgeHost implements IRpcBridgeHost<Window> {
    public createdByServer = false
    public backend: Window
    private handler: BridgeHostEvents = null!
    private onOpenCompleter = Completer.create()
    public clientId: string = uuid()
    public constructor(clientWindow: Window, public serverHost: string) {
        this.backend = clientWindow
        if (!this.createdByServer) this.clientId = uuid()
    }
    public async close(): Promise<void> {
        await this.onClose(null!)
    }

    public async open(): Promise<void> {
        this.onOpenCompleter.complete()
    }

    public async prepare(eventHandler: BridgeHostEvents): Promise<void> {
        this.handler = eventHandler
        const window = this.createdByServer ? this.backend.parent : this.backend
        window.addEventListener("message", this.onMessage)
        window.addEventListener("close", this.onClose)

        this._sendMessage(new ReadyRequestMessage(this.clientId, this.serverHost))
    }
    private onMessage = async (event: MessageEvent) => {
        const message = _parseMessage(event.data)
        if (message.clientId != this.clientId) return
        const { kind } = message
        if (kind == MessageKind.READY_RESPONSE) {
            if (this.createdByServer) throw new Error("Server side bridge must not receive 'READY_RESPONSE'")
            await this.onOpenCompleter.promise
            await this.handler.onReady()
        }
        if (kind == MessageKind.CALL) {
            await this.onOpenCompleter.promise
            await this.handler.onMessage((message as CallRequestMessage).data)
        }
        if (kind == MessageKind.CLOSED) {
            this.onClose(null!)
        }
    }
    public async onReadyRequestReceived() {
        await this.onOpenCompleter.promise
        if (!this.createdByServer) throw new Error("Client side bridge must not receive 'READY_REQUEST'")
        this._sendMessage(new ReadyResponseMessage(this.clientId, this.serverHost))
        await this.onOpenCompleter.promise
        await this.handler.onReady()
    }
    public onClose = async (event: Event) => {
        this._sendMessage(new ClosedMessage(this.clientId, this.serverHost))
        const window = this.createdByServer ? this.backend.parent : this.backend
        window.removeEventListener("close", this.onClose)
        window.removeEventListener("message", this.onMessage)
        await this.handler.onClose()
    }

    public async send(data: Uint8Array): Promise<void> {
        this._sendMessage(new CallRequestMessage(this.clientId, data, this.serverHost))
    }
    private _sendMessage(message: Message) {
        const fragments: Fragment[] = []
        let data: Uint8Array = null!
        if (message.kind == MessageKind.CALL) {
            let _temp = message as CallRequestMessage
            data = _temp.data
            _temp.data = null!
        }
        fragments.push(new Fragment(textEncoder.encode(JSON.stringify(message))))
        if (message.kind == MessageKind.CALL) {
            fragments.push(new Fragment(data))
        }
        const windowObject = this.createdByServer ? this.backend : this.backend.parent
        windowObject.postMessage(Fragment.encodeAll(...fragments), "*")
    }
}
const textDecoder = new TextDecoder()
const textEncoder = new TextEncoder()
function _parseMessage<T extends Message>(messagePacket: Uint8Array): T {
    const fragments = [...Fragment.fromBytes(messagePacket) as any]
    const message = JSON.parse(textDecoder.decode(fragments[0].data)) as Message
    if (message.kind == MessageKind.CALL) {
        (message as CallRequestMessage).data = fragments[1].data
    }
    return message as T
}
/**
 * Window server host that implemented {@link IRpcServerHost} interface through window.postMessage api.
 */
export class WindowServerHost implements IRpcServerHost<Window>{
    private waitForLaunch = Completer.create()
    private handler: ServerHostEvents = null!
    private clients = new Map<string, WindowBridgeHost>()

    constructor(public backend: Window, public serverHost: string) {
    }

    public async launch(): Promise<void> {
        this.waitForLaunch.complete()
    }

    public async prepare(eventHandler: ServerHostEvents): Promise<void> {
        this.handler = eventHandler
        this.backend.addEventListener("message", this.onMessageReceived)
        this.backend.addEventListener("close", this.onClosed);
        (async () => {
            await this.waitForLaunch.promise
            await this.handler.onReady()
        })()
    }

    private onMessageReceived = (event: MessageEvent) => {
        const data = event.data as Uint8Array
        if (data.constructor.name != "Uint8Array") return
        const message = _parseMessage<ReadyRequestMessage>(data)
        if (message.kind != MessageKind.READY_REQUEST) return
        if (message.serverHost != this.serverHost) return

        const clientHost = new WindowBridgeHost(event.source as Window, this.serverHost)
        clientHost.clientId = message.clientId
        clientHost.createdByServer = true
        const onCloseHandler = clientHost.onClose
        clientHost.onClose = async event => {
            await onCloseHandler(event)
            this.clients.delete(clientHost.clientId)
        }
        this.handler.onClientConnected(clientHost)
        clientHost.onReadyRequestReceived()
        this.clients.set(clientHost.clientId, clientHost)
    }

    public async shutdown(): Promise<void> {
        this.onClosed()
    }

    private onClosed = () => {
        this.backend.removeEventListener("close", this.onClosed)
        this.backend.removeEventListener("message", this.onMessageReceived)
        this.clients.forEach(client => client.close())
        this.handler.onShutdown()
    }
}
